/*
Eteria IRC Client, an RFC 1459 compliant client program written in Java.
Copyright (C) 2000-2001  Javier Kohen <jkohen at tough.com>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package ar.com.jkohen.awt;

import java.awt.*;
import java.awt.event.*;
import java.text.Collator;
import java.text.CollationKey;
import java.util.Enumeration;
import java.util.Vector;
import ar.com.jkohen.irc.Modes;
import ar.com.jkohen.irc.RFC1459;
import ar.com.jkohen.irc.User;
import ar.com.jkohen.util.CollatedHashtable;

public class NickList extends ScrollPane implements MouseListener, MouseMotionListener, ItemSelectable
{
    public static final int SYMBOL_RENDERER = 0;
    public static final int BULLET_RENDERER = 1;

    public static final int SORT_ALPHA = 0;
    public static final int SORT_ALPHA_FAVOR_MODE = 1;

    private static final int PREF_MIN_WIDTH = 190;

	private int item_renderer = BULLET_RENDERER;
    private int sort_method = SORT_ALPHA;
	
    private MouseListener mouseListener;
    private ActionListener actionListener;
    private ItemListener itemListener;

    protected MyList screen_list;
    private NickInfoPopup nick_pop;
	private Point mouse_coords;

    private CollatedHashtable items;

    private Collator collator;

    private Color textbg = SystemColor.text;
    private Color textfg = SystemColor.textText;
    private Color selbg = SystemColor.textHighlight;
    private Color selfg = SystemColor.textHighlightText;

    private String last_selected_nick;

    // SORT_*_FAVOR_MODE masks. These are ordered by priority.
    private static final int [] SORT_MASKS = { User.OWNER_MASK, User.ADMIN_MASK, User.OP_MASK, User.HALFOP_MASK, User.VOICE_MASK, User.NORMAL_MASK };
    private static final String SORT_CHARS = "123456";

    public NickList()
	{
    	super(ScrollPane.SCROLLBARS_ALWAYS); // _AS_NEEDED is so buggy...

    	this.collator = RFC1459.getCollator();
    	this.items = new CollatedHashtable(collator);

    	Panel p = new Panel();
    	CardLayout layout = new CardLayout();
		p.setLayout(layout);

    	this.screen_list = new MyList();
		
    	this.nick_pop = new NickInfoPopup();

      	p.add(screen_list, "list");
      	p.add(nick_pop, "popup");
		
	   	add(p);

    	screen_list.addMouseListener(this);
    	screen_list.addMouseMotionListener(this);
    }
	
    public String [] getNicks()
    {
		String [] list = new String [items.size()];

		int i = 0;
		for (Enumeration e = items.keys(); e.hasMoreElements(); )
		    list[i++] = (String)e.nextElement();

		return list;
    }

    /**
     * Gets the nick name for the item that contains <code>p</code>.
     *
     * @param p the point to look for in the nicks list.
     */
     
    public String getNickAt(Point p)
    {
		NickItem ni = getNickItemAt(p);
		if (ni != null)
			return ni.getNick();
		else
			return(null);
    }

    /**
     * Gets the NickItem that contains <code>p</code>.
     *
     * @param p the point to look for in the NickList.
     */
     
    protected NickItem getNickItemAt(Point p)
    {
		for (Enumeration e = items.elements(); e.hasMoreElements(); )
		{
	    	NickItem ni = (NickItem)e.nextElement();

	  	    Rectangle r = new Rectangle(ni.getBounds().x, ni.getBounds().y, screen_list.getBounds().width, ni.getBounds().height);
	  	    if (r.contains(p))
  				return ni;
		}

		return null;
    }

    public void loadList(User [] users)
    {
		items.clear();
		for (int i = 0; i < users.length; i++)
	    	add(users[i].getTag(), users[i].getModes());

//		validate();
		repaint();
    }

    /**
     * Adds <code>nick</code> to the list with user mode <code>mode</code>.
     */
	 
    public void add(String nick, int mode)
    {
    	NickItem ni = new NickItem(nick);

    	switch (item_renderer)
    	{
    	case BULLET_RENDERER:
    	    ni.setBullet(true);
    	    break;
    	case SYMBOL_RENDERER:
    	    ni.setBullet(false);
    	    break;
    	default:
    	    throw new IllegalArgumentException("Invalid NickItem renderer selected");
    	}

    	ni.setForeground(getForeground());
    	ni.setBackground(getBackground());
    	ni.setSelectedForeground(selfg);
    	ni.setSelectedBackground(selbg);
    	ni.setModes(mode);
    	ni.setTextBackground(textbg);
    	
    	if ((mode & User.OWNER_MASK) != 0)
    		ni.setTextForeground(User.COLOR_OWNER);
		else if ((mode & User.ADMIN_MASK) != 0)
	    	ni.setTextForeground(User.COLOR_ADMIN);
		else if ((mode & User.OP_MASK) != 0) 
	    	ni.setTextForeground(User.COLOR_OP);
    	else if ((mode & User.HALFOP_MASK) != 0) 
	    	ni.setTextForeground(User.COLOR_HOP);
    	else if ((mode & User.VOICE_MASK) != 0)
	    	ni.setTextForeground(User.COLOR_VOICE);
    	else
	    	ni.setTextForeground(NickInfo.nickToColor(nick));
	    
    	items.put(nick, ni);
    }

    protected void setItemSelected(NickItem ni, boolean state)
	{
		ni.setSelected(state);

		int item_state = state ? ItemEvent.SELECTED : ItemEvent.DESELECTED;
		processItemEvent(new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, this, item_state));

		screen_list.repaint();
    }

    protected void unselectAllItems()
	{
		for (Enumeration e = items.elements(); e.hasMoreElements(); )
		{
	    	NickItem ni = (NickItem) e.nextElement();
	    	setItemSelected(ni, false);
		}
    }

    public int getSortMethod()
	{
		return sort_method;
    }

    public void setSortMethod(int method)
	{
		this.sort_method = method;
		repaint();
    }

    public String getSelectedItem()
	{
		return last_selected_nick;
    }

    public Object[] getSelectedObjects()
	{
		Vector temp_list = new Vector(items.size());

		for (Enumeration e = items.elements(); e.hasMoreElements(); )
		{
	    	NickItem ni = (NickItem) e.nextElement();
	    	if (ni.isSelected())
				temp_list.addElement(ni.getNick());
		}

		Object[] objs = new String[temp_list.size()];
		temp_list.copyInto(objs);
		
		return objs;
    }

    public int getItemRenderer()
	{
		return item_renderer;
    }

    public void setItemRenderer(int item_renderer)
	{
		this.item_renderer = item_renderer;
    }
    
	public void repaint()
	{
		screen_list.setDirty();
		screen_list.repaint();
		validate();
	}

    public void setFont(Font f)
	{
		screen_list.setFont(f);
		nick_pop.setFont(f);
		repaint();
    }
	
    public Color getTextBackground()
	{
		return(textbg);
    }

    public void setTextBackground(Color c)
	{
		super.setBackground(c);
		screen_list.setBackground(c);
		this.textbg = c;

		for (Enumeration e = items.elements(); e.hasMoreElements(); )
		{
		    NickItem ni = (NickItem) e.nextElement();
		    ni.setTextBackground(c);
		}
		repaint();
    }

    public Color getTextForeground()
	{
		return textfg;
    }

    public void setTextForeground(Color c)
	{
		this.textfg = c;

		for (Enumeration e = items.elements(); e.hasMoreElements(); )
		{
	    	NickItem ni = (NickItem) e.nextElement();
		    ni.setTextForeground(c);
		}
    }

    public Color getSelectedBackground()
	{
		return selbg;
    }

    public void setSelectedBackground(Color c)
	{
		this.selbg = c;

		for (Enumeration e = items.elements(); e.hasMoreElements(); )
		{
	    	NickItem ni = (NickItem) e.nextElement();
		    ni.setSelectedBackground(c);
		}
    }

    public Color getSelectedForeground()
	{
		return selfg;
    }

    public void setSelectedForeground(Color c)
	{
		this.selfg = c;

		for (Enumeration e = items.elements(); e.hasMoreElements(); )
		{
	    	NickItem ni = (NickItem) e.nextElement();
		    ni.setSelectedForeground(c);
		}
    }

    public Dimension getPreferredSize()
	{
		Dimension d = super.getPreferredSize();
		d.width = Math.min(d.width, PREF_MIN_WIDTH);
		return(d);
    }

    /**
     * Translate the event as if it were thrown by this object.
     */
	 
    protected MouseEvent translateListMouseEvent(MouseEvent ev)
	{
		return new MouseEvent(this, ev.getID(), ev.getWhen(), ev.getModifiers(), ev.getX(), ev.getY(), ev.getClickCount(), ev.isPopupTrigger());
    }

    public void addMouseListener(MouseListener l)
	{
		if (null != l)
		    mouseListener = AWTEventMulticaster.add(mouseListener, l);
    }

    public void removeMouseListener(MouseListener l)
	{
		if (null != l)
		    mouseListener = AWTEventMulticaster.remove(mouseListener, l);
    }

    public void addActionListener(ActionListener l)
	{
		if (null != l)
		    actionListener = AWTEventMulticaster.add(actionListener, l);
    }

    public void removeActionListener(ActionListener l)
	{
		if (null != l)
		    actionListener = AWTEventMulticaster.remove(actionListener, l);
    }

    protected void processActionEvent(ActionEvent e)
	{
		if (actionListener != null)
		    actionListener.actionPerformed(e);
    }

    public void addItemListener(ItemListener l)
	{
		if (null != l)
		    itemListener = AWTEventMulticaster.add(itemListener, l);
    }

    public void removeItemListener(ItemListener l)
	{
		if (null != l)
		    itemListener = AWTEventMulticaster.remove(itemListener, l);
    }

    protected void processItemEvent(ItemEvent e)
	{
		if (itemListener != null)
		    itemListener.itemStateChanged(e);
    }

    public void mouseClicked(MouseEvent ev)
	{
		Object source = ev.getSource();

		if (source.equals(screen_list))
		{
		    if (mouseListener != null)
				mouseListener.mouseClicked(translateListMouseEvent(ev));

		    NickItem ni = getNickItemAt(ev.getPoint());
		    if (null == ni)
				return;

		    int ev_modifiers = ev.getModifiers();
		    if ((ev_modifiers & MouseEvent.BUTTON1_MASK) != 0 || ev_modifiers == 0)
			{
				// WORKAROUND: #32 Netscape Navigator doesn't set it right for BUTTON1.
				String nick = ni.getNick();

				switch (ev.getClickCount())
				{
				case 1:
				    boolean previously_selected = ni.isSelected();
				    if (!previously_selected)
						last_selected_nick = nick;
					
				    if (0 == (ev_modifiers & MouseEvent.CTRL_MASK))
						unselectAllItems();
					
				    setItemSelected(ni, !previously_selected);
				    break;
				case 2:
			    	processActionEvent(new ActionEvent(NickList.this, ActionEvent.ACTION_PERFORMED, nick, ev.getModifiers()));
				    break;
				}
		    }
		}
    }

    public void mousePressed(MouseEvent ev)
	{
		Object source = ev.getSource();

		if (source.equals(screen_list))
	    	if (mouseListener != null)
				mouseListener.mousePressed(translateListMouseEvent(ev));
    }

    public void mouseReleased(MouseEvent ev)
	{
		Object source = ev.getSource();

		if (source.equals(screen_list))
		    if (mouseListener != null)
				mouseListener.mouseReleased(translateListMouseEvent(ev));
    }

    public void mouseEntered(MouseEvent ev)
	{
		Object source = ev.getSource();

		if (source.equals(this))
		    if (mouseListener != null)
				mouseListener.mouseEntered(ev);
    }

    public void mouseExited(MouseEvent ev)
    {
		Object source = ev.getSource();

		if (source.equals(this))
		{
	    	if (mouseListener != null)
				mouseListener.mouseExited(ev);
		}
		else if (source.equals(screen_list))
		{
	    	mouse_coords = null;
			repaint();
		}
    }
	
	public void mouseMoved(MouseEvent ev)
	{
		Object source = ev.getSource();
		if (source.equals(screen_list))
		{
			mouse_coords = ev.getPoint();
			repaint();
		}
    }
    
	public void mouseDragged(MouseEvent ev)
	{
    }




    protected class MyList extends Canvas implements ComponentListener
    {
    	private Image image;
    	private Graphics img_gfx;
    	private NickItem [] items; // This obscures items in NickList.
    	private Dimension items_size;
    	private boolean dirty_items;

    	MyList()
    	{
    	    this.dirty_items = true;
    	    NickList.this.addComponentListener(this);
    	}
    	
    	public void setDirty()
    	{
    	    this.dirty_items = true;
    	}

    	private void createImageBuffer(Dimension size)
    	{
    	    image = createImage(size.width, size.height);
   		    if (img_gfx != null)
   				img_gfx.dispose();

    	    img_gfx = image.getGraphics();
    	}

    	public void update(Graphics g)
    	{
    	    paint(g);
    	}

    	public void paint(Graphics g)
    	{

    	    if (dirty_items)
    	    {
    			setSize(getPreferredSize());
      			super.repaint();
      			return;
    	    }

    	    Dimension size = getSize();

    	    if (null == img_gfx	|| size.width != image.getWidth(this) || size.height != image.getHeight(this))
    			createImageBuffer(size);

    	    img_gfx.setColor(getBackground());
			img_gfx.fillRect(0, 0, size.width, size.height);
    	    img_gfx.setColor(getForeground());

    	    for (int i = 0; i < items.length; i++)
    	    {
    			Rectangle r = items[i].getBounds();

    			// Draw item.
    			Graphics ng = img_gfx.create(r.x, r.y, size.width, r.height);
	   			items[i].paint(ng);
//				items[i].setSelected(false);
				if (ng != null)
					ng.dispose();
    	    }
    	    
    	    paintRollover(img_gfx);

    	    g.drawImage(image, 0, 0, this);
    	}

		private void paintRollover(Graphics g)
		{
			nick_pop.setVisible(false);
		
			if (mouse_coords != null)
			{
				NickItem ni = getNickItemAt(mouse_coords);
				String n = null;
				if (ni != null)
				{
//					ni.setSelected(true);
				
					n = getNickAt(mouse_coords);
					if (n != null)
					{
						nick_pop.setNick(n);
						nick_pop.setVisible(true);
					}
				}
			
				nick_pop.setLocation(mouse_coords);
			}

			nick_pop.paint(g);
		}
	
    	public Dimension getMinimumSize()
    	{
    	    return getPreferredSize();
    	}

    	public Dimension getPreferredSize()
    	{
    	    if (dirty_items)
    		{
    			grabItems();
    			sortItems();

    			Graphics g = null != img_gfx ? img_gfx : getGraphics();

    			int y = 0;
    			int max_width = 0;

    			for (int i = 0; i < items.length; i++)
    			{
    		    	Dimension d = items[i].getSize(g);
    		    	items[i].setBounds(0, y, d.width, d.height);

    			    max_width = Math.max(max_width, d.width);
      			    y += d.height;
    			}

    			this.items_size = new Dimension(max_width, y);
    	    }

    	    return adjustToPane(items_size.width, items_size.height);
    	}

    	private Dimension adjustToPane(int w, int h)
    	{
    	    Dimension vp = NickList.this.getViewportSize();

    	    if (w < vp.width)
    			w = vp.width;
    	
    		if (h < vp.height)
    			h = vp.height;

    	    return new Dimension(w, h);
    	}

    	private void grabItems()
    	{
    	    // FIXME: Implement synchronization.
    	    // Can't store directly to ITEMS array because NickList.this.items size could change and break the array store.

    	    Vector temp_list = new Vector(NickList.this.items.size());

    	    for (Enumeration e = NickList.this.items.elements(); e.hasMoreElements(); )
    	    {
    			NickItem ni = (NickItem) e.nextElement();
    			temp_list.addElement(ni);
    	    }

    	    this.items = new NickItem [temp_list.size()];
            temp_list.copyInto(items);

    	    dirty_items = false;
    	}

    	// Sorting algorithm from James Gosling
    	
    	private synchronized void sortItems()
        {
            sort(items, 0, items.length - 1);
        }
    	
        private void sort(NickItem n[], int i, int j)
        {
            int k = i;
            int l = j;
            if(k >= l)
            	return;
    		else
    			if(k == l - 1)
    			{
    				NickItem a = n[k];
    				NickItem b = n[l];
    				if (compare(a, b) < 0)
    				{
    					n[k] = b;
						n[l] = a;
    				}
    			}
    			
            NickItem a = n[(k + l) / 2];
			n[(k + l) / 2] = n[l];
			n[l] = a;
    		
            while(k < l) 
            {
                while(k < l && compare(a, n[k]) >= 0)
                    k++;
                while(k < l && compare(a, n[l]) <= 0)
    				l--;
                if(k < l)
                {
    				NickItem ni = n[k];
					n[k] = n[l];
					n[l] = ni;
                }
            }
			n[j] = n[l];
			n[l] = a;
    				
            sort(n, i, k - 1);
            sort(n, l + 1, j);
        }
    	
    	private int compare(NickItem i, NickItem j)
    	{
    		String n = i.getNick();
    		String m = j.getNick();
			
			if (n == null || m == null)
				return 0;

    		int o = i.getModes();
    		int p = j.getModes();
			
			int comp = 0;
    		
            switch (sort_method)
            {
            default:
            case SORT_ALPHA:
            	comp = collator.compare(n, m);
            	break;
            case SORT_ALPHA_FAVOR_MODE:
	    		// Will prefix the nick with a num char according to its highest privilege.    		
	    		int k = 0;
	    		while ((k < SORT_MASKS.length) && ((o & SORT_MASKS[k]) == 0))
	    			k++;
	    		if (k >= SORT_MASKS.length)
	    			k = SORT_MASKS.length - 1;
		    	n = SORT_CHARS.charAt(k) + n;

	    		k = 0;
    			while ((k < SORT_MASKS.length) && ((p & SORT_MASKS[k]) == 0))
    				k++;
    			if (k >= SORT_MASKS.length)
    				k = SORT_MASKS.length - 1;
    			m = SORT_CHARS.charAt(k) + m;

	    		comp = collator.compare(n, m);
			   	break;
            }
			
			return(comp);
    	}

    	public void componentResized(ComponentEvent e)
    	{
    	    setSize(getPreferredSize());
    	}

    	public void componentMoved(ComponentEvent e)
    	{
    	}

    	public void componentShown(ComponentEvent e)
    	{
    	}

    	public void componentHidden(ComponentEvent e)
    	{
    	}	
	}
	
}
